﻿/*
 * SVN-based CodeReview
 * Copyright (c) Karim Agha <karim.dev@gmail.com>.
 */

namespace Sentio.CodeReview.Foundation
{
    using System;
    using SharpSvn;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.IO;


    /// <summary>
    /// Implementation of the SVN-protocol-specific comments storage.
    /// </summary>
    public class SvnTargetRepository : ITargetRepository
    {
        private readonly SolutionConfiguration _config;
        private readonly SvnClient _client;

        private const string CodeReviewTagPrefix = "cr";
        private const char CodeReviewSeperator = ':';

        public SvnTargetRepository(SolutionConfiguration configuration)
        {
            // first some sanitary checks.
            if (configuration == null)
            {
                throw new ArgumentNullException("configuration");
            }

            if (String.IsNullOrWhiteSpace(configuration.Username))
            {
                throw new ArgumentNullException("configuration.username");
            }

            if (String.IsNullOrWhiteSpace(configuration.SvnProjectRoot))
            {
                throw new ArgumentNullException("configuration.svnprojectroot");
            }

            if (!configuration.SvnProjectRoot.EndsWith("/"))
            {
                configuration.SvnProjectRoot = configuration.SvnProjectRoot + "/";
            }

            // make sure that the passed string is a valid target (or at least in the valid format).
            // if this would  fail and throw an exception, then the user
            // would be notified with that exception that something wrong is with his svn address.
            SvnTarget.FromString(configuration.SvnProjectRoot);

            // next set the root path that will serve as the base for all later relative requests.
            _config = configuration;
            _client = new SvnClient();
            _client.Authentication.DefaultCredentials = new System.Net.NetworkCredential(_config.Username, _config.Password);
        }

        /// <summary>
        /// Retrieves targets under the specified path with parsed comments.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public IEnumerable<Target> GetChildTargets(string path)
        {
            if (String.IsNullOrWhiteSpace(path))
            {
                throw new ArgumentNullException("path");
            }

            if (path.StartsWith("/"))
            {
                path = path.Substring(1);
            }

            string targetPath = _config.SvnProjectRoot + path;
            if (!Uri.IsWellFormedUriString(targetPath, UriKind.Absolute))
            {
                throw new ArgumentException("path");
            }

            // setup the collection that will hold our targets, the svn ones and our.
            SvnTarget currentTarget = SvnTarget.FromString(targetPath);
            Dictionary<string, bool> targetPaths = new Dictionary<string, bool>();
            List<Target> parsedTargets = new List<Target>();

            // first, get a list of available targets
            _client.List(currentTarget, delegate(object sender, SvnListEventArgs e)
            {
                if (!String.IsNullOrWhiteSpace(e.Path))
                {
                    targetPaths.Add(e.RepositoryRoot + e.BasePath + "/" + e.Path, e.Entry is SvnDirEntry);
                }
            });

            // next retrieve each target's properties, parse them and store the
            // parsed target object in the final output-collection.
            foreach (string singleTarget in targetPaths.Keys)
            {
                parsedTargets.Add(ProcessSvnTarget(singleTarget, targetPaths[singleTarget]));
            }

            return parsedTargets.AsReadOnly();
        }

        /// <summary>
        /// Adds a comment to the latest revision for a file.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="comment"></param>
        /// <returns></returns>
        public bool AddComment(string path, BlockComment comment)
        {
            if (String.IsNullOrWhiteSpace(path))
            {
                throw new ArgumentNullException("path");
            }

            if (comment == null)
            {
                throw new ArgumentNullException("comment");
            }

            if (String.IsNullOrWhiteSpace(comment.CommentBody))
            {
                throw new ArgumentNullException("comment.CommentBody");
            }

            if (comment.FromOffset >= comment.ToOffset)
            {
                throw new ArgumentOutOfRangeException("comment.FromOffset", "The start offset cannot be same or larger than the end offset.");
            }

            // if the path is relative, then convert it to absolute.
            if (!path.StartsWith(_config.SvnProjectRoot))
            {
                path = _config.SvnProjectRoot + "/" + path;
            }

            // now do some paperwork, i.e append the metadata about a config
            // such as the creation timestamp, the author's username, etc..
            comment.CreatedAt = DateTime.Now;
            comment.Author = _config.Username;
            
            // now convert that to SVN-friendly property value
            SvnPropertyValue formattedComment = ConvertToSvnProperty(comment);
            SvnInfoEventArgs info;
            SvnTarget svnTarget = SvnTarget.FromString(path);
            try
            {
                // sometimes we randomly might get 502 server errors, we have to cope with it
                // somehow.
                _client.GetInfo(svnTarget, out info);
            }
            catch(Exception e)
            {
                throw new Exception("Adding a comment failed due to a temprorary server problem. Please try again in few moments.", e);
            }


            return _client.SetProperty(new Uri(path), formattedComment.Key, formattedComment.StringValue,
                new SvnSetPropertyArgs
                {
                    BaseRevision = info.Revision,
                    LogMessage = String.Format("The file {0} was code-reviewd by {1}. Comment status: {2}",
                        svnTarget.FileName, _config.Username, comment.Status.ToString())
                }) && _client.Update(GetLocalPath(path));
        }

        protected string GetLocalPath(string remotePath)
        {
            return remotePath.Replace(_config.SvnProjectRoot, _config.SolutionBaseDirectory);
        }

        public bool ChangeComment(string path, BlockComment oldComment, BlockComment newComment)
        {
            throw new System.NotImplementedException();
        }

        /// <summary>
        /// Retreives one specific file's associated properties/comments.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public Target GetTarget(string path)
        {
            return ProcessSvnTarget(Path.Combine(_config.SolutionBaseDirectory, path), false);
        }

        protected SvnPropertyValue ConvertToSvnProperty(BlockComment comment)
        {
            if (comment == null)
            {
                throw new ArgumentNullException("comment");
            }

            if (String.IsNullOrWhiteSpace(comment.CommentBody))
            {
                throw new ArgumentNullException("comment.CommentBody");
            }

            if (comment.FromOffset >= comment.ToOffset)
            {
                throw new ArgumentOutOfRangeException("comment.FromOffset", "The start offset cannot be same or larger than the end offset.");
            }
            
            if (String.IsNullOrWhiteSpace(comment.Author))
            {
                throw new ArgumentNullException("comment.Author");
            }

            if (comment.CreatedAt < new DateTime(2010, 1, 1))
            {
                throw new ArgumentOutOfRangeException("comment.CreatedAt");
            }

            // alright, the comment is valid, build up the key and return SVN property object.
            string key =
                CodeReviewTagPrefix + CodeReviewSeperator +
                comment.Author + CodeReviewSeperator +
                ((int)comment.Status).ToString() + CodeReviewSeperator +
                comment.FromOffset + CodeReviewSeperator +
                comment.ToOffset + CodeReviewSeperator +
                comment.CreatedAt.ToFileTimeUtc().ToString();

            return new SvnPropertyValue(key, comment.CommentBody);
        }

        protected Target ProcessSvnTarget(string targetPath, bool isDirectory)
        {
            if (String.IsNullOrWhiteSpace(targetPath))
            {
                throw new ArgumentNullException("targetPath");
            }
            
            SvnTarget svnTarget = SvnTarget.FromString(targetPath);
            
            Collection<SvnPropertyListEventArgs> propertyList =  new Collection<SvnPropertyListEventArgs>();
            SvnPropertyListArgs args = new SvnPropertyListArgs
            {
                ThrowOnError = false,
                ThrowOnWarning = false
            };

            _client.GetPropertyList(svnTarget, args, out propertyList);

            // this collection holds a collection of all parsed comments for this target.
            List<BlockComment> targetComments = new List<BlockComment>();

            // walk through the properties attached to a target and look for code-review
            // specific ones.
            if(propertyList.Count == 1)
            {
                foreach (SvnPropertyValue property in propertyList[0].Properties)
                {
                    if (property.Key.StartsWith(CodeReviewTagPrefix))
                    {
                        // we've found a potential candidate for a review comment...
                        // split it into chunks: author, status, start index, end index, timestamp
                        string[] chunks = property.Key.Substring(CodeReviewTagPrefix.Length + 1).Split(CodeReviewSeperator);
                        if (chunks.Length != 5)
                        {
                            // not a valid review-property, skipping to next one.
                            continue;
                        }

                        int startOffset, endOffset;
                        if (!Int32.TryParse(chunks[2], out startOffset) || !Int32.TryParse(chunks[3], out endOffset))
                        {
                            // not a valid review property, as above, skipping to next one.
                            continue;
                        }

                        BlockCommentStatus status;
                        if (!Enum.TryParse<BlockCommentStatus>(chunks[1], out status))
                        {
                            // same here, not a valid cr-property.
                            continue;
                        }
                        long createdAtLong;
                        DateTime createdAt;

                        if (!Int64.TryParse(chunks[4], out createdAtLong))
                        {
                            continue;
                        }
                        try
                        {
                            createdAt = DateTime.FromFileTimeUtc(createdAtLong);
                        }
                        catch
                        {
                            continue;
                        }
                        BlockComment comment = new BlockComment()
                        {
                            Author = chunks[0],
                            CommentBody = property.StringValue,
                            FromOffset = startOffset,
                            ToOffset = endOffset,
                            Status = status, 
                            CreatedAt = createdAt
                        };
                    
                        // a parsed comment.
                        targetComments.Add(comment);
                    }
                }
            } 
            
            TargetType type = isDirectory ? TargetType.Directory : TargetType.File;
            return new Target(targetPath, type, targetComments);
        }
    }
}
